package com.tianji.promotion.service.impl;

import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.tianji.common.domain.dto.PageDTO;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.exceptions.DbException;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.constants.RedisConstant;
import com.tianji.promotion.discount.Discount;
import com.tianji.promotion.discount.DiscountStrategy;
import com.tianji.promotion.domain.dto.CouponDiscountDTO;
import com.tianji.promotion.domain.dto.OrderCourseDTO;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.CouponScope;
import com.tianji.promotion.domain.po.ExchangeCode;
import com.tianji.promotion.domain.po.UserCoupon;
import com.tianji.promotion.domain.query.UserCouponQuery;
import com.tianji.promotion.domain.vo.CouponPageVO;
import com.tianji.promotion.enums.ExchangeCodeStatus;
import com.tianji.promotion.enums.UserCouponStatus;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.mapper.ExchangeCodeMapper;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.service.ICouponScopeService;
import com.tianji.promotion.service.IUserCouponService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.promotion.utils.CodeUtil;
import com.tianji.promotion.utils.MyLock;
import com.tianji.promotion.utils.PermuteUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.aop.framework.AopContext;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;

import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * <p>
 * 用户领取优惠券的记录，是真正使用的优惠券信息 服务实现类
 * </p>
 *
 * @author rui
 * @since 2024-08-13
 */
@Service
@RequiredArgsConstructor
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {
    private final CouponMapper couponMapper;
    private final ExchangeCodeMapper  exchangeCodeMapper;
    private final StringRedisTemplate redisTemplate;
    private final ICouponScopeService scopeService;
    @Override
    public void receiveCouponById(Long couponId) {
        // 1.查询优惠券
        Coupon coupon = couponMapper.selectById(couponId);
        if (coupon == null) {
            throw new BadRequestException("优惠券不存在");
        }
        // 2.校验发放时间
        LocalDateTime now = LocalDateTime.now();
        if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) {
            throw new BadRequestException("优惠券发放已经结束或尚未开始");
        }

        /*checkCouponAndSaveCouponInfo(couponId, coupon, null);
        * 由于这个方法是事务方法，导致非事务方法调用了事务方法，此时调用的是checkCouponAndSaveCouponInfo方法，
        * 没有经过Transactional代理，所以没有经过aop那一套，如begin transaction 等
        * 只是执行了这个方法的普通过程，正确方法应该是调用这个方法的代理对象，执行代理方法
        * */
        Long userId = UserContext.getUser();
        IUserCouponService userCouponService = (IUserCouponService) AopContext.currentProxy();
        userCouponService.checkCouponAndSaveCouponInfo(userId,coupon,null);
    }

    @Transactional
    @MyLock(name = "lock:coupon:{userId}")
    public void checkCouponAndSaveCouponInfo(Long userId, Coupon coupon, Long serialNum) {
            // 1.校验库存
            if (coupon.getIssueNum() >= coupon.getTotalNum()) {
                throw new BadRequestException("优惠券库存不足");
            }

            // 2.校验每人限领数量
            // 2.1.统计当前用户对当前优惠券的已经领取的数量
            Integer count = lambdaQuery()
                    .eq(UserCoupon::getUserId, userId)
                    .eq(UserCoupon::getCouponId, coupon.getId())
                    .count();
            // 2.2.校验限领数量
            if(count != null && count >= coupon.getUserLimit()){
                throw new BadRequestException("超出领取数量");
            }
            // 3.更新优惠券的已经发放的数量 + 1,利用乐观锁
            couponMapper.incrIssueNum(coupon.getId());
            // 4.新增一个用户券
            saveUserCoupon(userId, coupon);

            //5.如果是兑换码，则修改对应的兑换码信息,兑换人、兑换状态
            if(serialNum != null){
                exchangeCodeMapper.update(null,new LambdaUpdateWrapper<ExchangeCode>()
                        .set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
                        .set(ExchangeCode::getUserId,userId)
                        .eq(ExchangeCode::getId,serialNum));
            }
    }

    @Override
    public List<CouponDiscountDTO> findDiscountSolution(List<OrderCourseDTO> orderCourses) {
        if(CollUtils.isEmpty(orderCourses)){
            return CollUtils.emptyList();
        }
        //1.根据用户id查询优惠券信息和用户券信息
        Long userId = UserContext.getUser();
        List<Coupon> coupons = this.getBaseMapper().queryMyCoupon(userId);

        //2.查到优惠券，先初步判断是否可用，收集可用的优惠券
        //2.1 查询当前商品的总价格，便于去进行判断是否达到用户券使用门槛
        int totalAmount = orderCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();
        List<Coupon> canUsedcouponList = coupons.stream()
                .filter(coupon -> DiscountStrategy.getDiscount(coupon.getDiscountType()).canUse(totalAmount, coupon))
                .collect(Collectors.toList());
        if(canUsedcouponList.isEmpty()){
            return CollUtils.emptyList();
        }
        // 3.排列组合出所有方案
        // 3.1.细筛（找出每一个优惠券的可用的课程，判断课程总价是否达到优惠券的使用需求）
        Map<Coupon,List<OrderCourseDTO>> avaMap = findAvailableCoupon(canUsedcouponList,orderCourses);
        // 3.2.排列组合
        Set<Coupon> couponSet = avaMap.keySet();
        canUsedcouponList = new ArrayList<>(couponSet);
        List<List<Coupon>> solutions = PermuteUtil.permute(canUsedcouponList); //有效优惠券集合
        // 3.3.除了叠加的券还有单个优惠券
        for (Coupon coupon : canUsedcouponList) {
            solutions.add(List.of(coupon));
        }
        // 4.计算方案的优惠明细

        // 5.筛选最优解
        return null;
    }

    private Map<Coupon, List<OrderCourseDTO>> findAvailableCoupon(List<Coupon> coupons, List<OrderCourseDTO> orderCourses) {
        Map<Coupon, List<OrderCourseDTO>> result = new HashMap<>(coupons.size());
        //1.遍历优惠券集合
        for (Coupon coupon : coupons) {
            List<OrderCourseDTO> dtoList = orderCourses;
            if(coupon.getSpecific()){
                //1.1 查询对应能使用的特定范围
                List<CouponScope> list = scopeService.lambdaQuery()
                        .eq(CouponScope::getCouponId, coupon.getId()).list();
                //1.2 把对应的业务集合收集起来看看购物车的商品bizid是否存在
                List<Long> bizIds = list.stream().map(CouponScope::getBizId).collect(Collectors.toList());
                dtoList = dtoList.stream().filter(orderCourseDTO -> bizIds.contains(orderCourseDTO.getCateId())).collect(Collectors.toList());
            }
            if(dtoList.isEmpty()){
                continue;
            }
            //1.3 除了符合特定的使用信息还得对应的特定商品总价是否符合优惠券的使用门槛
            int sum = dtoList.stream().mapToInt(OrderCourseDTO::getPrice).sum();
            Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());
            if(discount.canUse(sum,coupon)){
                //1.4 添加能使用的优惠券下属详细的商品信息到map里
                result.put(coupon,dtoList);
            }
        }
        //2. 返回有效优惠券的map
        return result;
    }

    /**
     * 兑换码兑换优惠券
     * @param code
     */
    @Override
    public void exchangeCoupon(String code) {
        //1.根据兑换码解析自增id
        long serialNum = CodeUtil.parseCode(code);
        //2.基于兑换码自增id在redis判断兑换码是否已经兑换过
        String key = RedisConstant.COUPON_CODE_MAP;
        boolean isSuccess = updateUserCouponStatus(key,serialNum,true);
        if(isSuccess){
            throw new BizIllegalException("兑换码已经兑换过了");
        }
        //3.查询兑换码是否存在
        ExchangeCode exchangeCode = exchangeCodeMapper.selectById(serialNum);
        if(exchangeCode == null){
            throw new DbException("兑换码不存在");
        }
        try {
            //4.查询兑换码是否过期
            LocalDateTime now = LocalDateTime.now();
            LocalDateTime expiredTime = exchangeCode.getExpiredTime();
            if(!now.isBefore(expiredTime)){
                throw new BizIllegalException("兑换码已经过期");
            }
            //5.查询对于兑换码的优惠券信息
            Long exchangeTargetId = exchangeCode.getExchangeTargetId();
            Coupon coupon = couponMapper.selectById(exchangeTargetId);
            if(coupon == null){
                throw new BizIllegalException("优惠券不存在");
            }
            //6.查询优惠券库存是否充足,查询个人限制是否到达上限,新增用户的优惠券信息,修改优惠券和兑换码的相关信息
            checkCouponAndSaveCouponInfo(coupon.getId(),coupon,serialNum);
        } catch (Exception e) {
            updateUserCouponStatus(key,serialNum,false);
            throw  e;
        }
    }

    @Override
    public PageDTO<CouponPageVO> pageQueryCouponWithStatus(UserCouponQuery query) {
        //1.基于用户券的状态分页查询信息
        Long userId = UserContext.getUser();
        Page<UserCoupon> page = lambdaQuery()
                .eq(UserCoupon::getUserId, userId)
                .eq(UserCoupon::getStatus, query.getStatus())
                .page(query.toMpPageDefaultSortByCreateTimeDesc());
        List<UserCoupon> records = page.getRecords();
        if(CollUtils.isEmpty(records)){
            //如果数据为null，则返回空数据
            return PageDTO.empty(0L,0L);
        }

        //2.封装放回信息
        List<Long> couponIds = records.stream().map(UserCoupon::getCouponId).collect(Collectors.toList());
        List<Coupon> couponList = couponMapper.selectBatchIds(couponIds);
        List<CouponPageVO> voList = BeanUtil.copyToList(couponList, CouponPageVO.class);
        return PageDTO.of(page,voList);
    }

    private boolean updateUserCouponStatus(String key, long serialNum, boolean flag) {
        //result为true说明已经兑换过，为false则说明未兑换
        Boolean result = redisTemplate.opsForValue().setBit(key, serialNum - 1, flag);
        return result !=null && result;
    }


    private void saveUserCoupon(Long userId, Coupon coupon) {
        UserCoupon userCoupon = new UserCoupon();
        userCoupon.setCouponId(coupon.getId());
        userCoupon.setUserId(userId);
        userCoupon.setStatus(UserCouponStatus.UNUSED);
        //如果是有使用期限则直接在优惠券里面取值
        LocalDateTime termBeginTime = coupon.getTermBeginTime();
        LocalDateTime termEndTime = coupon.getTermEndTime();
        if (termBeginTime == null) {
            //如果没有使用期限，说明有使用期限天数限制
            termBeginTime = LocalDateTime.now();
            termEndTime = termBeginTime.plusDays(coupon.getTermDays());
        }
        userCoupon.setTermBeginTime(termBeginTime);
        userCoupon.setTermEndTime(termEndTime);
        save(userCoupon);
    }
}
